#!/usr/bin/env python3
#-*- encoding:utf-8 -*-

"""
文件功能： web service跟 微信公众号通信所需的函数
作者: arthur.chen@huawei.com
函数：
   1. check_signature(): 验证wechat 请求合理性
   2. encrypt(): 微信消息加密，用于向微信公众号返回消息时对消息加密
   3. decrypt(): 微信消息解密，用于接收到微信公众号发送的消息时解密
   4. handle_text_message(): 处理文本消息，获得返回信息
   5. handle_message(): 处理来自微信公众号发来的消息
   6. publish_message(): 向本机的SSE服务器端发送消息，以便于订阅者能收到此消息 
   7. get_access_token(): 获取access_token，以便于未来访问微信接口
"""


# 用于计算sha1加密
import hashlib
# 构造 json
import json
import logging
import queue
import time
# xml格式解析 
import xml.etree.ElementTree as ET
# 发送http请求（到ANE后台）
import requests

# 全局配置
from globals import g_queue, g_sse_publish_url
# 微信加解密(python3版本), 官网上为Python2
from WXBizMsgCrypt import WXBizMsgCrypt

# 日志
logger = logging.getLogger(__name__)


""" 验证wechat 请求合理性
@param signature: 微信公众号请求时附带的 签名字符串
@param timestamp: 微信公众号请求时附带的 时间戳
@param nonce: 微信公众号请求时附带的 随机字符串
@param token: 在微信公众号配置界面上配置的用于验证请求是否来自微信的token
@return: True 合理；   False 不合理
"""
def check_signature(signature, timestamp, nonce, token):
	# 按微信公众号要求，组成数组，并按字典序排序
	s = [timestamp, nonce, token]
	s.sort()
	s = ''.join(s).encode('utf-8')
	if hashlib.sha1(s).hexdigest() == signature:
		return True
	else:
		return False


""" 微信消息加密，用于向微信公众号返回消息时对消息加密
@param msg: 待加密的消息字符串
@param nonce: 微信公众号请求时附带的 随机字符串
@param token: 微信公众号页面上填写的token，用于验证消息是否从微信发送出来，传入的值需保持和页面上的一致
@param appid: 微信公众号的appid，传入的值需保持和页面上的一致
@param encode_aes_key: 微信公众号页面上消息加解密秘钥，EncodingAESKey，传入的值需保持和页面上的一致
@return: 加密后的字符串
"""
def encrypt(msg, nonce, token, appid, encode_aes_key):
	encryptor = WXBizMsgCrypt(token, encode_aes_key, appid)
	ret,encrypt_xml = encryptor.EncryptMsg(msg, nonce)
	return encrypt_xml 



""" 微信消息解密，用于接收到微信公众号发送的消息时解密
@param msg: 待解密的消息字符串
@param nonce: 微信公众号请求时附带的 随机字符串
@param token: 微信公众号页面上填写的token，用于验证消息是否从微信发送出来，传入的值需保持和页面上的一致
@param appid: 微信公众号的appid，传入的值需保持和页面上的一致
@param encode_aes_key: 微信公众号页面上消息加解密秘钥，EncodingAESKey，传入的值需保持和页面上的一致
@param msg_signature: 微信发送消息过来时，url上附带的http请求参数，名为 msg_signature
@param timestamp: 微信发送消息过来时，url上附带的http请求参数，名为 timestamp
@return: 解密后的字符串
"""
def decrypt(msg, nonce, appid, token, encode_aes_key, msg_signature, timestamp):
   decryptor = WXBizMsgCrypt(token, encode_aes_key, appid)
   ret, decryp_xml = decryptor.DecryptMsg(msg, msg_signature, timestamp, nonce)
   return decryp_xml


""" 处理来自微信公众号发来的消息
@param to_user: 消息发送给谁，一般是公众号自己
@param from_user: 消息来自谁
@param content: 消息文本内容
@param nonce: 微信公众号请求时附带的 随机字符串
@param token: 微信公众号页面上填写的token，用于验证消息是否从微信发送出来，传入的值需保持和页面上的一致
@param appid: 微信公众号的appid，传入的值需保持和页面上的一致
@param encode_aes_key: 微信公众号页面上消息加解密秘钥，EncodingAESKey，传入的值需保持和页面上的一致
@param msg_signature: 微信发送消息过来时，url上附带的http请求参数，名为 msg_signature
@param timestamp: 微信发送消息过来时，url上附带的http请求参数，名为 timestamp
@return: 按照微信公众要要求格式的xml字符串，且进行了加密
"""
def handle_text_message(to_user, from_user, content, nonce, token, appid, encode_aes_key):
	logger.info(f'[Recv]to:{to_user}; from:{from_user}; content:{content}')
 
	# 向ANE后台服务发送消息，并获取其返回值
	pub_msg = {'to_user': to_user, 'from_user': from_user, 'content': content}
	publish_message(g_sse_publish_url, pub_msg)
 
	# 阻塞并等待ANE后台服务回应（异步，请求会被写入 g_queue）
	try:
		data = g_queue.get(block=True, timeout=5)
	except queue.Empty:
		logger.error("Can't get the reply from ane backend ")
		data = 'succ'
	if data != 'succ':
		to_user = data['to_user']
		from_user = data['from_user']
		content = data['content']			
	else:
		content = data
  
	# 构造 回应消息（按照微信要求XML格式）
	stime = str(int(time.time()))
	reply_text = f"""<xml>
  		<ToUserName><![CDATA[{to_user}]]></ToUserName>
  		<FromUserName><![CDATA[{from_user}]]></FromUserName>
  		<CreateTime>{stime}</CreateTime>
  		<MsgType><![CDATA[text]]></MsgType>
  		<Content><![CDATA[{content}]]></Content>
		</xml>"""
	logger.debug(f'Reply wehcat:{reply_text}')
 
	# 回应消息 加密
	encrypt_reply = encrypt(reply_text,nonce, token, appid, encode_aes_key) 
 
	return encrypt_reply

""" 处理来自微信公众号发来的消息
@param xml_str: 微信公众号发来的原始字符串，为加过密的字符串
@param nonce: 微信公众号请求时附带的 随机字符串
@param token: 微信公众号页面上填写的token，用于验证消息是否从微信发送出来，传入的值需保持和页面上的一致
@param appid: 微信公众号的appid，传入的值需保持和页面上的一致
@param encode_aes_key: 微信公众号页面上消息加解密秘钥，EncodingAESKey，传入的值需保持和页面上的一致
@param msg_signature: 微信发送消息过来时，url上附带的http请求参数，名为 msg_signature
@param timestamp: 微信发送消息过来时，url上附带的http请求参数，名为 timestamp
@return: 按照微信公众要要求格式的xml字符串，且进行了加密
"""
def handle_message(xml_str, nonce, appid, token, encode_aes_key, msg_signature, timestamp):
	# 解密 xml 字符串
	logger.info('original: ')
	logger.info(xml_str)
	xml_str = decrypt(xml_str, nonce, appid, token, encode_aes_key, msg_signature, timestamp)
	logger.info('decrpyt:')
	logger.info(xml_str)

	# 解析xml数据
	root = ET.fromstring(xml_str)
  	# 消息类型
	msg_type = root.find('MsgType').text
	# 不同消息的处理
	if msg_type == 'text':		
		# 处理文本消息
		content = root.find('Content').text
		to_user = root.find('ToUserName').text
		from_user = root.find('FromUserName').text
		return handle_text_message(to_user, from_user, content, nonce, token, appid, encode_aes_key)
	elif msg_type == 'image':
		media_id = root.find('MediaId').text
	elif msg_type == 'voice':
		# 处理语音消息， 其中 Recognition 代表语音文字
		content = root.find('Recognition').text
		to_user = root.find('ToUserName').text
		from_user = root.find('FromUserName').text  
		print(f"receive voice message.  voice to text: {content}")
		return handle_text_message(to_user, from_user, content, nonce, token, appid, encode_aes_key)
	elif msg_type == 'location':
		# 地理位置
		x = root.find('Location_X').text
		y = root.find('Location_Y').text
	elif msg_type == 'link':
		# 链接
		title = root.find('Title').text
		url = root.find('Url').text
	elif msg_type == 'event':
		# 事件
		pass
	else:
		logger.error(f"unkonw message type:{msg_type}")
		return "unkonw message type"


""" 向本机的SSE服务器端发送消息，以便于订阅者能收到此消息
@param message: 要发送的消息
@return: True 成功， False 失败
"""
def publish_message(url, message):	
	headers = {'Content-Type': 'application/json' }
	response = requests.post(url, data=json.dumps(message), headers=headers)
	if response.status_code == 200 or response.status_code == 204:
		logger.info(f'publish message successfully :{message}')	
		return True
	else:
		logger.error(f'publish message failed:{message}')	
		return False

""" 获取access_token，以便于未来访问微信接口
@param appid: 微信公众号页面上的 开发者ID
@param secret_key: 微信公众号页面上的AppSecret
@return: access_token 访问令牌, expires_in 过期时间(秒）
"""
def get_access_token(appid, app_secret):
	url = f"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={app_secret}"
	response = requests.get(url)
	if response.status_code == 200 :
		access_token = response.json().get('access_token')
		expires_in = response.json().get('expires_in')
	else:
		access_token = None
		expires_in = None
	return access_token, expires_in
